﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using OverStore.Exceptions;
using OverStore.Runtime.PersistenceServiceProvider;
using OverStore.Runtime.ObjectRepository.CallbackManager;
using OverStore.Runtime.ObjectRepository.Attacher;
using OverStore.Runtime.ObjectRepository.ChangesTracker;
using OverStore.Runtime.Storage;
using System.Linq;
using OverStore.Runtime.ObjectRepository;
using OverStore.Runtime.ObjectRepository.Depot;
using OverStore.Runtime.ObjectRepository.CommandSet;
using OverStore.Runtime.ObjectRepository.Mapping;
using OverStore.Runtime.ObjectRepository.DeferredLoader;
using OverStore.Runtime.ObjectRepository.ErrorTransformer;
using OverStore.Runtime.ObjectRepository.Reader;
using OverStore.Scoping.Manager;

namespace OverStore.Runtime.ObjectRepository
{
    /// <summary>
    /// Load and saves instances of <typeparamref name="TInstance"/> type to data storage. 
    /// </summary>
    /// <typeparam name="TInstance">The type of the instance.</typeparam>
    /// <typeparam name="TIdentifier">The type of the instance identifier.</typeparam>
    public sealed class PersistenceObjectRepository<TInstance, TIdentifier> : IEntityRegistry<TInstance, TIdentifier>, IDisposable
        where TInstance : class
    {
        private readonly IPersistenceServiceProvider _Provider;
        private readonly IPersistenceServiceProvider _SavingProvider;
        private readonly IPersistenceServiceProvider _AttachingProvider;
        private readonly IPersistenceServiceProvider _AddingProvider;

        private readonly RefCounter<TInstance> _AddingRefCounter = new RefCounter<TInstance>();
        private readonly RefCounter<TInstance> _AcceptingChangesRefCounter = new RefCounter<TInstance>();

        private readonly IDepot<TInstance, TIdentifier> _Depot;
        private readonly IChangesTracker<TInstance> _ChangesTracker;
        private readonly ICommandSet _CommandSet;
        private readonly IDataStorage _Storage;
        private readonly IMapper<TInstance> _Mapper;
        private readonly IAttacher<TInstance, TIdentifier> _Attacher;
        private readonly ICallbackManager<TInstance> _CallbackManager;
        private readonly IDeferredLoader<TInstance> _DeferredLoader;
        private readonly IErrorTransformer<TInstance> _ErrorTransformer;
        private readonly IReader<TInstance, TIdentifier> _ParameterlessReader;
        private readonly Dictionary<Type, IReader<TInstance, TIdentifier>> _ParameterizedReaders;
        private readonly OperationScopeManager _OperationScopeManager;
        private readonly IDictionary<TInstance, int> _SavedInstances = new Dictionary<TInstance, int>(ReferenceEqualityComparer<TInstance>.Instance);
        private readonly IDictionary<TInstance, int> _DirtyInstances = new Dictionary<TInstance, int>(ReferenceEqualityComparer<TInstance>.Instance);
        private readonly IDictionary<TInstance, int> _PersistedInstances = new Dictionary<TInstance, int>(ReferenceEqualityComparer<TInstance>.Instance);

        private readonly InstanceAttachInfo<TIdentifier> _AttachInfo = new InstanceAttachInfo<TIdentifier>();

        /// <summary>
        /// Holds cache contains map of instances are in process of attachment with corresponding actual instances.
        /// Used for avoid infinity recursion while attaching.
        /// </summary>
        private readonly IDictionary<TInstance, TInstance> _AttachInstanceMap = new Dictionary<TInstance, TInstance>();

        private bool _Disposed = false;

        /// <summary>
        /// Initializes new instance of <see cref="PersistenceObjectRepository{TInstance, TIdentifier}"/>
        /// </summary>
        /// <param name="provider">The object allows to perform persistence operations over instances of different types.</param>
        /// <param name="callbackManager">Callback manager.</param>
        /// <param name="depot">Object depot.</param>
        /// <param name="lazyLoader">Object lazy loader.</param>
        /// <param name="changesTracker">The changes tracker.</param>
        /// <param name="commandSet">The command set.</param>
        /// <param name="storage">The storage.</param>
        /// <param name="mapper">The mapper.</param>
        /// <param name="errorTransformer">The error transformer.</param>
        /// <param name="attacher">The attacher.</param>
        /// <param name="operationScopeManager">The operation scope manager.</param>
        /// <param name="parameterlessReader">The parameterless reader.</param>
        /// <param name="parameterizedReaders">The parameterized readers.</param>
        /// <exception cref="ArgumentNullException"><paramref name="provider"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="depot"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="commandSet"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="storage"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="mapper"/> is null reference.</exception>
        public PersistenceObjectRepository(
            IPersistenceServiceProvider provider,
            ICallbackManager<TInstance> callbackManager,
            IDepot<TInstance, TIdentifier> depot,
            IDeferredLoader<TInstance> lazyLoader,
            IChangesTracker<TInstance> changesTracker,
            ICommandSet commandSet,
            IDataStorage storage,
            IMapper<TInstance> mapper,
            IErrorTransformer<TInstance> errorTransformer,
            IAttacher<TInstance, TIdentifier> attacher,
            OperationScopeManager operationScopeManager,
            IReader<TInstance, TIdentifier> parameterlessReader,
            IEnumerable<IReader<TInstance, TIdentifier>> parameterizedReaders)
        {
            if (Object.ReferenceEquals(provider, null))
                throw new ArgumentNullException("provider");
            _Provider = provider;
            if (depot == null)
                throw new ArgumentNullException("depot");
            _Depot = depot;
            _ChangesTracker = changesTracker;
            if (commandSet == null)
                throw new ArgumentNullException("commandSet");
            _CommandSet = commandSet;
            if (storage == null)
                throw new ArgumentNullException("storage");
            _Storage = storage;
            if (mapper == null)
                throw new ArgumentNullException("mapper");
            if (Object.ReferenceEquals(operationScopeManager, null))
                throw new ArgumentNullException("operationScopeManager");
            _Mapper = mapper;
            _CallbackManager = callbackManager;
            _DeferredLoader = lazyLoader;
            _ErrorTransformer = errorTransformer;
            _Attacher = attacher;
            _ParameterlessReader = parameterlessReader;
            _ParameterizedReaders = (parameterizedReaders ?? Enumerable.Empty<IReader<TInstance, TIdentifier>>())
                                        .Where(r => r.ParameterType != null)
                                        .ToDictionary(r => r.ParameterType);

            _SavingProvider = new SavingPersistenceServiceProvider(_Provider);
            _AttachingProvider = new AttachingPersistenceServiceProvider(_Provider);
            _AddingProvider = new AddingPersistenceServiceProvider(_Provider);
            _OperationScopeManager = operationScopeManager;
            _OperationScopeManager.ScopeExited += OnScopeCompleted;
        }

        /// <summary>
        /// Get type of objects served by repository.
        /// </summary>
        public Type Type
        {
            get
            {
                return typeof(TInstance);
            }
        }

        /// <summary>
        /// Get identifier for object registered in session.
        /// If object is not registered in session, it will be placed in session.
        /// If object does not have identifier yet, the identifier will be assigned to it
        /// corresponding to identification rules.
        /// </summary>
        /// <param name="instance">The instance to get identifier.</param>
        /// <returns>The instance identifier.</returns>
        /// <exception cref="PersistingRuntimeException">Error occured during get identifier.</exception>
        public TIdentifier GetIdentifier(TInstance instance)
        {
            Debug.Assert(instance != null, "Get Identifier of null object");
            return _Depot.GetObjectIdentifier(instance);
        }

        /// <summary>
        /// Gets entity with specified identifier. If entity registered in session it will be returned.
        /// Otherwise it will loaded from data storage. If there is no entity with specified identifier in data storage,
        /// method returns default value of <typeparamref name="TInstance"/> type.
        /// </summary>
        /// <param name="identifier">The identifier of entity to get from registry.</param>
        /// <returns>
        /// The entity with specified identifier or default value of <typeparamref name="TEntity"/> type of there is no
        /// entity with specified identifier exists in data storage.
        /// </returns>
        public TInstance GetInstanceOrDefault(TIdentifier identifier)
        {
            Debug.Assert(!ReferenceEquals(identifier, null), "Get Instance by null identifier");
            TInstance instance = null;
            bool created = false;
            try
            {
                instance = _Depot.GetOrCreate(identifier, out created);
                if (created)
                    if (!Reload(instance))
                    {
                        _Depot.RemoveObject(instance);
                        return default(TInstance);
                    }
                return instance;
            }
            catch
            {
                if (created && !ReferenceEquals(instance, null))
                    _Depot.RemoveObject(instance);
                throw;
            }
        }

        /// <summary>
        /// Get object registered in session.
        /// If object is not found, the object will be constructed, registered in session, reloaded and
        /// returned.
        /// If object does not registered in session, it will be constructed
        /// using data extracted from default gateway.
        /// </summary>
        /// <param name="identifier">The identifier of instance to get.</param>
        /// <returns>The instance with specified identifier.</returns>
        /// <exception cref="PersistingRuntimeException">Error occured during get object by identifier.</exception>
        public TInstance GetInstance(TIdentifier identifier)
        {
            Debug.Assert(!ReferenceEquals(identifier, null), "Get Instance by null identifier");
            var result = GetInstanceOrDefault(identifier);
            if (ReferenceEquals(result, null))
                throw new PersistingRuntimeException(
                           String.Format(
                               CultureInfo.InvariantCulture,
                               "Object Repository error: reloading data of instance of type {0} failed: SQL SELECT command affects 0 rows.",
                               typeof(TInstance)));
            return result;
        }

        /// <summary>
        /// Get object registered in session.
        /// If object is not found, the object will be constructed, registered in session and reloaded
        /// using data passed as <paramref name="data"/> argument.
        /// If <paramref name="data"/> is null, the object will be constructed using data extracted from default gateway.
        /// </summary>
        /// <param name="identifier">The identifier of instance to get.</param>
        /// <param name="data">The data used for object construction or null for using data from default gateway.</param>
        /// <returns>The instance with specified identifier.</returns>
        /// <exception cref="PersistingRuntimeException">Error occured during get object by identifier.</exception>
        public TInstance GetInstance(TIdentifier identifier, TypedRecord data)
        {
            Debug.Assert(!ReferenceEquals(identifier, null), "Get Instance by null identifier");
            Debug.Assert(data != null, "Get Instance using null data");
            bool created;
            var instance = _Depot.GetOrCreate(identifier, out created);
            if (created)
                try
                {
                    RecordReload(instance, data);
                }
                catch (PersistingRuntimeException)
                {
                    _Depot.RemoveObject(instance);
                    throw;
                }

            return instance;
        }

        /// <summary>
        /// Remember new identifier for instance registered in session.
        /// Call AcceptChanges method to actually apply new identifier to instance and
        /// register new identifier in session.
        /// </summary>
        /// <param name="instance">The instance to update its identifier.</param>
        /// <param name="newIdentifier">The new identifier.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during update identifier.</exception>
        /// <remarks>
        /// Identifier updating is required if identifiers are generated by database.
        /// Delayed identifier updating is required because if error occured due updating the instance
        /// or other instance in batch, identifier value generated by database becomes invalid.
        /// </remarks>
        public void PutNewIdentifier(TInstance instance, TIdentifier newIdentifier)
        {
            Debug.Assert(instance != null, "Put identifier for null instance");
            Debug.Assert(!ReferenceEquals(newIdentifier, null), "Putting null identifier.");
            _Depot.PutNewIdentifier(instance, newIdentifier);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (!_Disposed)
            {
                _Disposed = true;
                _AttachInstanceMap.Clear();
                _AcceptingChangesRefCounter.Clear();
                _AddingRefCounter.Clear();

                _SavedInstances.Clear();
                _PersistedInstances.Clear();
                _DirtyInstances.Clear();

                _OperationScopeManager.ScopeExited -= OnScopeCompleted;

                DisposeComponent(_ChangesTracker);
                DisposeComponent(_CommandSet);
                DisposeComponent(_Depot);
                DisposeComponent(_Mapper);
                DisposeComponent(_Storage);
                DisposeComponent(_Attacher);
            }
        }

        /// <summary>
        /// 	<para> Updates data of specified instance using data extracted from data storage,
        /// and reloads instance's aggregates and marks instance as unchanged,
        /// so further saving will not cause data storage updating until object will be changed.
        /// Method returns <c>true</c> if instance was successfuly reloaded, <c>false</c> if
        /// data storage commands used for reloading instance does not return any data.
        /// </para>
        /// 	<para> If specified <paramref name="instance"/> is not registered in repository, method also registers it.
        /// If deffer loading is supported and enabled, the object will only be registered for
        /// loading, and loading itself will be performed by request.</para>
        /// 	<para>
        /// If repository is configured for deferred loading, method immediately returns <c>true</c>,
        /// and throws <see cref="PersistingRuntimeException"/> if reloading will fails.
        /// </para>
        /// </summary>
        /// <param name="instance">The instance to reload.</param>
        /// <returns></returns>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same  already registered.</exception>
        /// <exception cref="PersistingRuntimeException">Error is occured during reload object data.</exception>
        public bool Reload(TInstance instance)
        {
            TIdentifier storedIdentifier = _Depot.GetStoredIdentifier(instance);
            bool notExist = _Depot.TryPutInstance(instance);
            try
            {
                bool result;
                if (_DeferredLoader != null && _DeferredLoader.IsEnabled)
                    result = _DeferredLoader.LoadOrDelay(instance, ParameterlessReload);
                else
                    result = ParameterlessReload(instance);
                if (notExist && !result)
                {
                    _Depot.RemoveObject(instance);
                    _Depot.SetStoredIdentifier(instance, storedIdentifier);
                }
                return result;
            }
            catch
            {
                if (notExist)
                {
                    _Depot.RemoveObject(instance);
                    _Depot.SetStoredIdentifier(instance, storedIdentifier);
                }
                throw;
            }
        }

        /// <summary>
        /// 	<para>
        /// Saves changes made in <paramref name="instance"/> in data storage and invokes saving instance's aggregates.
        /// If instance was previously marked as deleted, method removes it from data storage.
        /// </para>
        /// 	<para>
        /// If instance supports deffer loading and is not loaded yet, method does not perform any
        /// actions, because changes can not be made without instance loading.
        /// </para>
        /// 	<para>
        /// Method does not accept instance changes, but types implements current method should consider possibility
        /// of calling this method few times with same instance, in case of cycle references, for example.
        /// Repeated calls should not cause repeated saving, so method should take steps to avoid repeated saving.
        /// For example, instance may internally maintain list of saved instances, checks this list on saving and
        /// clear this list on accepting changes.
        /// </para>
        /// </summary>
        /// <param name="instance">The instance to save changes.</param>
        /// <exception cref="PersistingRuntimeException">Error occured while save object.</exception>
        /// <exception cref="VersionConflictException">Version conflict occured during save object.</exception>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void Save(TInstance instance)
        {
            if (_SavedInstances.ContainsKey(instance))
                return;

            TIdentifier storedIdentifier = default(TIdentifier);
            bool notExist = _Depot.TryPutInstance(instance);
            if (notExist)
                storedIdentifier = _Depot.GetStoredIdentifier(instance);

            _SavedInstances[instance] = 1;

            try
            {
                if (_DeferredLoader != null && _DeferredLoader.IsWaitForLoading(instance))
                    return;
                if (_CallbackManager != null && !_CallbackManager.Callback(instance, PersistenceOperation.Saving))
                    return;
                InstanceState state = _ChangesTracker == null ? InstanceState.Existing : _ChangesTracker.GetInstanceState(instance);
                switch (state)
                {
                    case InstanceState.Added:
                    case InstanceState.Foreign:
                        {
                            if (_CallbackManager != null && !_CallbackManager.Callback(instance, PersistenceOperation.Persisting))
                                return;
                            if (_CallbackManager != null)
                                _CallbackManager.Callback(instance, PersistenceOperation.Inserting);

                            _PersistedInstances[instance] = 1;

                            var instanceData = _Mapper.GetObjectData(instance, _SavingProvider);
                            int affectedRows;
                            TypedRecord result;
                            try
                            {
                                result = _Storage.Execute(_CommandSet.InsertCommand, instanceData, out affectedRows);
                            }
                            catch (PersistingRuntimeException ex)
                            {
                                if (_ErrorTransformer != null)
                                    _ErrorTransformer.Transform(ex);
                                throw;
                            }

                            if (affectedRows == 0)
                                throw new PersistingRuntimeException(
                                            String.Format(
                                                CultureInfo.InvariantCulture,
                                                "Object Repository error: inserting new instance of type {0} failed: SQL INSERT command affects 0 rows.",
                                                typeof(TInstance)));
                            _Mapper.ApplyObjectData(result, instance, _Provider);

                            if (_CallbackManager != null)
                                _CallbackManager.Callback(instance, PersistenceOperation.Inserted);
                            if (_CallbackManager != null)
                                _CallbackManager.Callback(instance, PersistenceOperation.Persisted);
                        }

                        break;
                    case InstanceState.Existing:
                        {
                            if (_ChangesTracker != null && _ChangesTracker.IsInstanceDataChanged(instance))
                            {
                                if (_CallbackManager != null && !_CallbackManager.Callback(instance, PersistenceOperation.Persisting))
                                    return;
                                if (_CallbackManager != null)
                                    _CallbackManager.Callback(instance, PersistenceOperation.Updating);

                                _PersistedInstances[instance] = 1;

                                var instanceData = _Mapper.GetObjectData(instance, _SavingProvider);
                                int affectedRows;
                                TypedRecord result;
                                try
                                {
                                    result = _Storage.Execute(_CommandSet.UpdateCommand, instanceData, out affectedRows);
                                }
                                catch (PersistingRuntimeException ex)
                                {
                                    if (_ErrorTransformer != null)
                                        _ErrorTransformer.Transform(ex);
                                    throw;
                                }

                                // If update does not occures any error, but does not affect 
                                // any row, that means one from two case: 
                                // 1. Object is deleted and can not be updated
                                // 2. Object version in storage does not match current version
                                if (affectedRows == 0)
                                {
                                    // Try to extract object from storage
                                    int selectRows;
                                    _Storage.Execute(_CommandSet.SelectCommand, instanceData, out selectRows);

                                    // If object not found, object is missing
                                    if (selectRows == 0)
                                        throw new PersistingRuntimeException(
                                            String.Format(
                                                CultureInfo.InvariantCulture,
                                                "Object Repository error: updating instance of type {0} failed: SQL UPDATE command affects 0 rows.",
                                                typeof(TInstance)));

                                    // else version confict occured
                                    throw new VersionConflictException(instance);
                                }
                                else
                                    _Mapper.ApplyObjectData(result, instance, _Provider);

                                if (_CallbackManager != null)
                                    _CallbackManager.Callback(instance, PersistenceOperation.Updated);
                                if (_CallbackManager != null)
                                    _CallbackManager.Callback(instance, PersistenceOperation.Persisted);
                            }
                            else
                            {
                                if (_CallbackManager != null)
                                    _CallbackManager.Callback(instance, PersistenceOperation.NotModified);
                            }
                        }

                        break;
                    case InstanceState.MarkedAsDeleted:
                        {
                            if (_CallbackManager != null && !_CallbackManager.Callback(instance, PersistenceOperation.Persisting))
                                return;
                            if (_CallbackManager != null)
                                _CallbackManager.Callback(instance, PersistenceOperation.Deleting);

                            _PersistedInstances[instance] = 1;

                            int affectedRows;
                            var instanceData = _Mapper.GetObjectData(instance, _SavingProvider);
                            try
                            {
                                _Storage.Execute(_CommandSet.DeleteCommand, instanceData, out affectedRows);
                            }
                            catch (PersistingRuntimeException ex)
                            {
                                if (_ErrorTransformer != null)
                                    _ErrorTransformer.Transform(ex);
                                throw;
                            }
                            if (affectedRows == 0)
                                throw new PersistingRuntimeException(
                                            String.Format(
                                                CultureInfo.InvariantCulture,
                                                "Object Repository error: deleting instance of type {0} failed: SQL DELETE command affects 0 rows.",
                                                typeof(TInstance)));
                            // Note: On deletion repository does not apply command 
                            // execution result to target instance. 
                            // This allows to skip checking columns existing in
                            // Mapper.ApplyObjectData when keys does not generated by database.
                            if (_CallbackManager != null)
                                _CallbackManager.Callback(instance, PersistenceOperation.Deleted);
                            if (_CallbackManager != null)
                                _CallbackManager.Callback(instance, PersistenceOperation.Persisted);
                        }

                        break;
                    case InstanceState.AddedThenDeleted:
                        break;
                    default:
                        break;
                }
                if (_CallbackManager != null)
                    _CallbackManager.Callback(instance, PersistenceOperation.Saved);
            }
            catch
            {
                if (notExist)
                {
                    _Depot.RemoveObject(instance);
                    _Depot.SetStoredIdentifier(instance, storedIdentifier);
                }
                throw;
            }
        }

        /// <summary>
        /// Save changes made to all objects registered in repository.
        /// </summary>
        /// <exception cref="T:OverStore.Exceptions.PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void SaveAll()
        {
            foreach (var item in _Depot.RegisteredObjects())
                Save(item);
        }

        /// <summary>
        /// Mark <paramref name="instance"/> as deleted.
        /// </summary>
        /// <param name="instance">The instance to mark as deleted.</param>
        /// <remarks>
        /// Method itself does not save any changes to storage. For actually delete <paramref name="instance"/>
        /// from storage, call <see cref="IRepository{TInstance}.Save"/> method.
        /// </remarks>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered.</exception>
        /// <exception cref="PersistingRuntimeException">Error occured while mark object as deleted.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void MarkAsDeleted(TInstance instance)
        {
            Debug.Assert(instance != null, "instance != null");

            bool notExist = _Depot.TryPutInstance(instance);
            if (notExist)
                throw new PersistingRuntimeException(
                            String.Format("Object repository error: error marking entity of type {0} as deleted:entity was not registered in repository previously.",
                                          typeof(TInstance)));

            if (_CallbackManager != null)
                _CallbackManager.Callback(instance, PersistenceOperation.MarkingAsDeleted);
            if (_ChangesTracker != null)
                _ChangesTracker.MarkAsDeleted(instance);
        }

        /// <summary>
        /// Marks <paramref name="instance"/> as needed to be physically saved to storage even 
        /// if <paramref name="instance"/> data are not changed.
        /// This method may be called any times untill AcceptChanges method called, but instance will be saved only once.
        /// </summary>
        /// <remarks>
        /// Use this method in couple with optimistic locking (version checking) to organize object hierarchy into 'atom' group. 
        /// Call this method on successful instance saving using other members of 'atom' object group, then save these objects.
        /// </remarks>
        /// <param name="instance">Instance to mark as dirty.</param>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered.</exception>
        /// <exception cref="PersistingRuntimeException">Error occured while mark object as dirty.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void MarkAsDirty(TInstance instance)
        {
            Debug.Assert(instance != null, "instance != null");

            _Depot.TryPutInstance(instance);
            if (_DirtyInstances.ContainsKey(instance))
                return;
            _DirtyInstances[instance] = 1;
            _ChangesTracker.MarkAsDirty(instance);
        }

        /// <summary>
        /// 	<para> Registers <paramref name="instance"/> in repository, if it is not registered yet. After registration
        /// <paramref name="instance"/> become available by its identifier. </para>
        /// 	<para> If same instance already registered in repository, method does not perform any action.</para>
        /// </summary>
        /// <param name="instance">The instance to registers in repository.</param>
        /// <exception cref="IdentityReferenceIntegrityException">Different instance with same identifier already registered.</exception>
        /// <exception cref="PersistingRuntimeException">Error occured during add object.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public void Add(TInstance instance)
        {
            Debug.Assert(instance != null, "instance != null");
            if (_AddingRefCounter.Touch(instance) != 1)
                return;
            try
            {
                _Depot.TryPutInstance(instance);
                if (_ChangesTracker != null)
                    _ChangesTracker.CaptureNewInstance(instance);

                _Mapper.ApplyObjectData(new WrapperTypedRecord(_Mapper.GetObjectData(instance, _AddingProvider)), instance, _Provider);

                if (_CallbackManager != null)
                    _CallbackManager.Callback(instance, PersistenceOperation.Adding);

                _AddingRefCounter.Untouch(instance);
            }
            catch
            {
                _AddingRefCounter.Reset(instance);
                throw;
            }
        }

        /// <summary>
        /// Joins specified instance or collection to repository if required, applies changes made to <paramref name="instance"/> and attach aggregates of specified instance.
        /// If specified instance is collection, attach each items and deletes items deleted from collection since detaching.
        /// </summary>
        /// <param name="instance">The instance to attach.</param>
        /// <returns>
        /// The instance registered in repository contains data of <paramref name="instance"/>.
        /// </returns>
        /// <exception cref="PersistingRuntimeException">Error occured during Attach object.</exception>
        /// <exception cref="PersistenceOperationIsNotSupportedException">Operation is not supported because of configuration part responsible for this operation is missing or does not support specific functionallity.</exception>
        public TInstance Attach(TInstance instance)
        {
            Debug.Assert(instance != null, "Attaching null instance.");
            if (_Attacher == null)
                throw new PersistenceOperationIsNotSupportedException("Attacher is not configured.");

            // If instance being attached has registered in session,
            // no additional processing required.
            if (_Depot.IsInstanceRegistered(instance))
                return instance;

            if (_AttachInstanceMap.ContainsKey(instance))
                return _AttachInstanceMap[instance];

            _Attacher.ProvideAttachInfo(instance, _AttachInfo);
            TInstance actualInstance;
            bool created = false;
            if (_AttachInfo.IsInstanceNew && _AttachInfo.GenerateNewIdentifier)
            {
                _Depot.TryPutInstance(instance);
                actualInstance = instance;
            }
            else
                actualInstance = _Depot.GetOrCreate(_AttachInfo.Identifier, out created);

            _AttachInstanceMap[instance] = actualInstance;

            _Mapper.ApplyObjectData(new WrapperTypedRecord(_Mapper.GetObjectData(instance, _AttachingProvider)), actualInstance, _AttachingProvider);
            if (_ChangesTracker != null)
                _ChangesTracker.Attach(actualInstance, !_AttachInfo.IsInstanceNew, _AttachInfo.AreInstanceDataChanged);
            if (_CallbackManager != null)
                _CallbackManager.Callback(actualInstance, PersistenceOperation.Attaching);

            return actualInstance;
        }

        /// <summary>
        /// Return enumerator throught object set.
        /// </summary>
        /// <returns>
        /// The sequence of object read from data storage.
        /// </returns>
        /// <exception cref="PersistingRuntimeException">Error occured while use object reader.</exception>
        public IEnumerable<TInstance> ExecuteObjectReader()
        {
            if (_ParameterlessReader == null)
                throw new PersistingRuntimeException("Parameterless reader is not defined for the type.");
            return _ParameterlessReader.ExecuteReader(null);
        }

        /// <summary>
        /// Return enumerator throught object sequence.
        /// </summary>
        /// <typeparam name="TParameter">The type of the parameter for command.</typeparam>
        /// <param name="parameter">The parameter for command.</param>
        /// <returns>
        /// The sequence of object read from data storage.
        /// </returns>
        /// <exception cref="PersistingRuntimeException">Error occured while use object reader.</exception>
        /// <exception cref="ParameterTypeNotSupportedException">Parameter with specified type is not supported.</exception>
        public IEnumerable<TInstance> ExecuteObjectReader<TParameter>(TParameter parameter)
            where TParameter : class
        {
            IReader<TInstance, TIdentifier> reader;
            if (!_ParameterizedReaders.TryGetValue(typeof(TParameter), out reader))
                throw new ParameterTypeNotSupportedException(typeof(TInstance), typeof(TParameter));
            return reader.ExecuteReader(parameter);
        }

        /// <summary>
        /// Disposes the component if argument is not null reference and supports <see cref="IDisposable"/> interface.
        /// </summary>
        private static void DisposeComponent(object component)
        {
            var disposable = component as IDisposable;
            if (disposable != null)
                disposable.Dispose();
        }

        private bool ParameterlessReload(TInstance instance)
        {
            if (_CallbackManager != null && !_CallbackManager.Callback(instance, PersistenceOperation.Loading))
                return true;

            _PersistedInstances.Remove(instance);

            var parameters = _Mapper.GetObjectData(instance, _Provider);
            int affectedRows = 0;
            var data = _Storage.Execute(_CommandSet.SelectCommand, parameters, out affectedRows);
            bool result = true;
            if (affectedRows != 0)
            {
                _Mapper.ApplyObjectData(data, instance, _Provider);
                if (_ChangesTracker != null)
                {
                    // This call is required because we may call reload on instance that is marked 
                    // as deleted, and during call of ChangesTracker.AcceptChanges such object 
                    // gets the new state, but existing state is expected. Calling CaptureNewInstance 
                    // clears delete mark from object.
                    _ChangesTracker.CaptureNewInstance(instance);

                    _ChangesTracker.AcceptChanges(instance);
                }
                result = true;
            }
            else
                result = false;
            if (_CallbackManager != null)
                _CallbackManager.Callback(instance, PersistenceOperation.Loaded);
            return result;
        }

        private void RecordReload(TInstance instance, TypedRecord record)
        {
            if (_CallbackManager != null && !_CallbackManager.Callback(instance, PersistenceOperation.Loading))
                return;

            _PersistedInstances.Remove(instance);

            _Mapper.ApplyObjectData(record, instance, _Provider);

            if (_ChangesTracker != null)
            {
                // This call is required because we may call reload on instance that is marked 
                // as deleted, and during call of ChangesTracker.AcceptChanges such object 
                // gets the new state, but existing state is expected. Calling CaptureNewInstance 
                // clears delete mark from object.
                _ChangesTracker.CaptureNewInstance(instance);

                // For accept changes we should not use objectData argument but explicity re-extract data 
                // from instance using mapper. It requires because data may be modified during applying it 
                // to instance.
                _ChangesTracker.AcceptChanges(instance);
            }

            if (_CallbackManager != null)
                _CallbackManager.Callback(instance, PersistenceOperation.Loaded);
        }

        private void OnScopeCompleted(object sender, OperationScopeActionEventArgs e)
        {
            if (e.Action == OperationScopeAction.PostCompleting)
            {
                foreach (var i in _PersistedInstances.Keys)
                {
                    if (_AcceptingChangesRefCounter.Touch(i) != 1)
                        continue;
                    if (_DeferredLoader != null && _DeferredLoader.IsWaitForLoading(i))
                        continue;
                    if (_ChangesTracker != null)
                        _ChangesTracker.AcceptChanges(i);

                    _Depot.AcceptNewIdentifier(i);                    
                    _AcceptingChangesRefCounter.Untouch(i);
                }
                _PersistedInstances.Clear();
                _SavedInstances.Clear();
                _DirtyInstances.Clear();
            }
            if (e.Action == OperationScopeAction.PostFailing)
            {
                foreach (var i in _PersistedInstances.Keys)
                    _Depot.RejectNewIdentifier(i);
                _PersistedInstances.Clear();
                _SavedInstances.Clear();
                _DirtyInstances.Clear();
            }
        }
    }
}